Skip to content

Conversation

@abrahammurciano
Copy link
Contributor

punq is an IOC (Inversion of Control) Container for Python 3.8+

@github-actions

This comment has been minimized.

punq is an IOC (Inversion of Control) Container for Python 3.8+
@github-actions

This comment has been minimized.

Copy link
Collaborator

@srittau srittau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, notes below.

service: type[_T] | str
scope: Scope
builder: Callable[[], _T]
needs: dict[str, object]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the invariance of dict, using object as value type is often problematic. For example, the following won't type check with mypy:

x = {"": "a"}
y: dict[str, object] = x  # incompatible assignments

The usual pattern here is to use Any.

needs: dict[str, object]
args: dict[str, object]

empty: Any
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The standard pattern for this kind of exported sentinel in typeshed is the following:

_Empty = NewType("_Empty", object)  # a class at runtime
empty: Final[_Empty]

See below for how to use it.

Alternatively, if empty is not supposed to be used by users, I would consider leaving this out.

def register_concrete_service(self, service: type | str, scope: Scope) -> None: ...
def build_context(self, key: type | str, existing: _ResolutionContext | None = None) -> _ResolutionContext: ...
def register(
self, service: type[_T] | str, factory: Callable[..., _T] = ..., instance: _T = ..., scope: Scope = ..., **kwargs: object
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above for using _Empty. Also, typeshed uses Any instead of object to mark kwargs where the type matters, but can't be represented accurately.

Suggested change
self, service: type[_T] | str, factory: Callable[..., _T] = ..., instance: _T = ..., scope: Scope = ..., **kwargs: object
self, service: type[_T] | str, factory: Callable[..., _T] | _Empty = ..., instance: _T | _Empty = ..., scope: Scope = ..., **kwargs: Any # kwargs are passed to ForwardRef

def register(self, service: type[_TOpt] | str, *, instance: _TOpt, **kwargs: Any) -> Container: ...
@overload
def register(
self, service: type[_TOpt] | str, factory: Callable[..., _TOpt] = ..., *, scope: Scope = ..., **kwargs: Any
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self, service: type[_TOpt] | str, factory: Callable[..., _TOpt] = ..., *, scope: Scope = ..., **kwargs: Any
self, service: type[_TOpt] | str, factory: Callable[..., _TOpt] | _Empty = ..., *, scope: Scope = ..., **kwargs: Any

Comment on lines 58 to 59
def __getitem__(self, key: type | str) -> object: ...
def __setitem__(self, key: type | str, value: object) -> None: ...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __getitem__(self, key: type | str) -> object: ...
def __setitem__(self, key: type | str, value: object) -> None: ...
def __getitem__(self, key: type | str) -> Any: ... # return type depends on the cache entry
def __setitem__(self, key: type | str, value: Any) -> None: ... # value type depends on the cache entry

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to type these generically instead of using Any

class Container:
registrations: _Registry
def __init__(self) -> None: ...
@overload
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@overload
# kwargs are passed to ForwardRef
@overload

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're actually all eventually passed to builder, which is the main destination of all the kwargs for all the functions. I put a single comment at the top of the Container indicating this for all the functions in that class.

Comment on lines 74 to 75
service: type[_TOpt] | str,
factory: Callable[..., _TOpt] = ...,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
service: type[_TOpt] | str,
factory: Callable[..., _TOpt] = ...,
service: type[_TOpt] | str | _Empty,
factory: Callable[..., _TOpt] | _Empty = ...,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service can't be empty, I assume you meant factory and instance?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, yes.

Comment on lines 80 to 82
def resolve_all(self, service: type[_TOpt] | str, **kwargs: Any) -> list[_TOpt]: ...
def resolve(self, service_key: type[_TOpt] | str, **kwargs: Any) -> _TOpt: ...
def instantiate(self, service_key: type[_TOpt] | str, **kwargs: Any) -> _TOpt: ...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like this for kwargs?

Suggested change
def resolve_all(self, service: type[_TOpt] | str, **kwargs: Any) -> list[_TOpt]: ...
def resolve(self, service_key: type[_TOpt] | str, **kwargs: Any) -> _TOpt: ...
def instantiate(self, service_key: type[_TOpt] | str, **kwargs: Any) -> _TOpt: ...
# All kwargs are passed to the builder.
def resolve_all(self, service: type[_TOpt] | str, **kwargs: Any) -> list[_TOpt]: ...
def resolve(self, service_key: type[_TOpt] | str, **kwargs: Any) -> _TOpt: ...
def instantiate(self, service_key: type[_TOpt] | str, **kwargs: Any) -> _TOpt: ...

class _Registration(NamedTuple, Generic[_T]):
service: type[_T] | str
scope: Scope
builder: Callable[[], _T]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that builder can be called with arbitrary keyword args, so this would be more appropriate:

Suggested change
builder: Callable[[], _T]
builder: Callable[..., _T]

@github-actions

This comment has been minimized.

- Replace object with Any where appropriate
- Clarify the use of **kwargs in method signatures
- Make _T default to Any and use it in more places instead of Any/object
- Include _Empty as a default parameter where applicable
@github-actions

This comment has been minimized.

@github-actions
Copy link
Contributor

According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉

Copy link
Collaborator

@srittau srittau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! For future reference, please don't force push in typeshed PRs as that makes reviewing changes harder. We squash when merging anyways.

@srittau srittau merged commit 127e4d2 into python:main Jan 15, 2026
48 checks passed
@abrahammurciano
Copy link
Contributor Author

Sure, sorry, and thank you for your help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants